diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 18:57:37 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 18:57:37 +0000 |
| commit | 6cc0bfd1d79074df790272b8091b1f0226d14283 (patch) | |
| tree | cab18b0f708cf3b0eaeeeb98e2cf81459365b33e /frontend/src/routes/services/[id] | |
| download | yaum-6cc0bfd1d79074df790272b8091b1f0226d14283.tar.gz yaum-6cc0bfd1d79074df790272b8091b1f0226d14283.zip | |
feat: upload project
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'frontend/src/routes/services/[id]')
| -rw-r--r-- | frontend/src/routes/services/[id]/+page.svelte | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/frontend/src/routes/services/[id]/+page.svelte b/frontend/src/routes/services/[id]/+page.svelte new file mode 100644 index 0000000..36ea105 --- /dev/null +++ b/frontend/src/routes/services/[id]/+page.svelte @@ -0,0 +1,261 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import { page } from '$app/stores'; + import { getHistory, getServiceStats, listServices } from '$lib/api'; + import { subscribeHeartbeats } from '$lib/realtime'; + import type { Service, Heartbeat, ServiceStats } from '$lib/types'; + + let svc: Service | undefined = $state(); + let history: Heartbeat[] = $state([]); + let stats: ServiceStats | null = $state(null); + let loading = $state(true); + let expandedError: number | null = $state(null); + + onMount(async () => { + try { + const all = await listServices(); + svc = all.find((s) => s.id === Number($page.params.id)); + history = await getHistory(Number($page.params.id)); + getServiceStats(Number($page.params.id)).then((s) => (stats = s)).catch(() => {}); + } catch { + // handle + } finally { + loading = false; + } + }); + + $effect(() => { + if (!svc && $page.params.id) { + getHistory(Number($page.params.id)) + .then((h) => (history = h)) + .catch(() => {}); + } + }); + + onMount(() => { + const unsub = subscribeHeartbeats((hb) => { + if (hb.service_id !== Number($page.params.id)) return; + history = [...history, hb].slice(-50); + }); + return unsub; + }); + + let chartPad = { t: 8, b: 20, l: 40, r: 12 }; + let chartW = $derived(Math.max(history.length * 32, 300)); + let chartH = 200; + let plotW = $derived(chartW - chartPad.l - chartPad.r); + let plotH = $derived(chartH - chartPad.t - chartPad.b); + let chartMax = $derived(Math.max(...history.map((h) => h.response_time_ms), 100)); + let chartMin = $derived(0); + + let dots = $derived( + history.map((h, i) => { + const x = history.length === 1 + ? chartPad.l + plotW / 2 + : chartPad.l + (i / (history.length - 1)) * plotW; + const y = chartPad.t + plotH - ((h.response_time_ms - chartMin) / (chartMax - chartMin || 1)) * plotH; + return { x, y, ...h }; + }) + ); + + let yTicks = $derived.by(() => { + const n = 4; + return Array.from({ length: n + 1 }, (_, i) => { + const val = chartMin + ((chartMax - chartMin) / n) * i; + const y = chartPad.t + plotH - ((val - chartMin) / (chartMax - chartMin || 1)) * plotH; + return { val: Math.round(val), y }; + }); + }); +</script> + +<svelte:head> + <title>{svc?.name ?? 'Detalhes'} — YAUM</title> +</svelte:head> + +{#if loading} + <div class="flex items-center justify-center py-20"> + <div + class="h-8 w-8 animate-spin rounded-full border-2 border-[var(--border-color)] border-t-[var(--green)]" + ></div> + </div> +{:else if !svc} + <div class="rounded-xl border border-[var(--border-color)] p-12 text-center"> + <p class="text-sm text-[var(--text-muted)]">Serviço não encontrado</p> + </div> +{:else} + <div class="mb-6"> + <a + href="/" + class="text-xs text-[var(--text-muted)] underline transition-colors hover:text-white" + > + ← Voltar + </a> + </div> + + <div class="mb-8"> + <h1 class="text-2xl font-semibold text-white">{svc.name}</h1> + <p class="mt-1 font-mono text-sm text-[var(--text-secondary)]">{svc.url}</p> + </div> + + <div class="grid gap-6 lg:grid-cols-3"> + <!-- Stats card --> + {#if stats} + <div class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5"> + <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]"> + Uptime + </h2> + <div class="space-y-3"> + {#each [ + { label: '24h', uptime: stats.uptime_24h, checks: stats.total_checks_24h, avg: stats.avg_response_ms_24h }, + { label: '7 dias', uptime: stats.uptime_7d, checks: stats.total_checks_7d, avg: stats.avg_response_ms_7d }, + { label: '30 dias', uptime: stats.uptime_30d, checks: stats.total_checks_30d, avg: stats.avg_response_ms_30d } + ] as item} + <div class="rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)]/30 p-3"> + <div class="mb-1 flex items-center justify-between"> + <span class="text-[10px] font-medium uppercase tracking-wider text-[var(--text-muted)]"> + {item.label} + </span> + <span class="font-mono text-xs tabular-nums text-[var(--text-secondary)]"> + {item.checks} checks + </span> + </div> + <div class="flex items-baseline gap-3"> + <span + class="text-lg font-bold tabular-nums" + style="color: {item.uptime >= 99 ? 'var(--green)' : item.uptime >= 95 ? '#facc15' : 'var(--red)'}" + > + {item.uptime.toFixed(2)}% + </span> + <span class="font-mono text-xs text-[var(--text-muted)]"> + {item.avg.toFixed(0)}ms méd. + </span> + </div> + </div> + {/each} + </div> + </div> + {/if} + + <!-- Tempo de Resposta (ms) --> + <div + class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5 lg:col-span-2" + > + <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]"> + Tempo de Resposta (ms) + </h2> + <div class="relative h-48"> + {#if history.length === 0} + <div class="flex h-full items-center justify-center text-xs text-[var(--text-muted)]"> + Sem dados ainda + </div> + {:else if history.length === 1} + <div class="flex h-full flex-col items-center justify-center gap-1"> + <span + class="text-3xl font-bold tabular-nums" + style="color: {history[0].is_up ? 'var(--green)' : 'var(--red)'}" + > + {history[0].response_time_ms} + <span class="text-base font-normal text-[var(--text-secondary)]">ms</span> + </span> + <span class="text-xs text-[var(--text-muted)]"> + {history[0].is_up ? 'Online' : 'Offline'} — + {history[0].status_code || 'timeout'} + </span> + </div> + {:else} + <svg class="h-full w-full" viewBox="0 0 {chartW} {chartH}" preserveAspectRatio="none"> + {#each yTicks as tick} + <line + x1={chartPad.l} + y1={tick.y} + x2={chartW - chartPad.r} + y2={tick.y} + stroke="var(--border-color)" + stroke-width="1" + stroke-dasharray="3,3" + /> + <text + x={chartPad.l - 6} + y={tick.y + 3} + text-anchor="end" + fill="var(--text-muted)" + font-size="9" + >{tick.val}</text + > + {/each} + + <polyline + points={dots.map((d) => `${d.x},${d.y}`).join(' ')} + fill="none" + stroke="var(--green)" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + + {#each dots as d} + <circle cx={d.x} cy={d.y} r="3.5" fill={d.is_up ? 'var(--green)' : 'var(--red)'} /> + {/each} + </svg> + {/if} + </div> + <div class="mt-2 flex justify-between text-[10px] text-[var(--text-muted)]"> + <span>{history[0]?.tested_at ? new Date(history[0].tested_at).toLocaleTimeString() : ''}</span> + <span + >{history[history.length - 1]?.tested_at + ? new Date(history[history.length - 1].tested_at).toLocaleTimeString() + : ''}</span + > + </div> + </div> + + <!-- Timeline compacta --> + <div class="rounded-xl border border-[var(--border-color)] bg-[var(--bg-card)] p-5"> + <h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-[var(--text-secondary)]"> + Últimas Verificações + </h2> + <div class="flex flex-col gap-1.5"> + {#each history.slice().reverse() as h} + <div> + <div class="flex items-center justify-between gap-2"> + <div class="flex items-center gap-2"> + <span + class="h-2 w-2 shrink-0 rounded-full" + class:bg-[var(--green)]={h.is_up} + class:bg-[var(--red)]={!h.is_up} + ></span> + <span class="font-mono text-xs"> + {#if h.status_code > 0} + {h.status_code} + {:else} + TIMEOUT + {/if} + </span> + </div> + <div class="flex items-center gap-2"> + <span class="font-mono text-xs text-[var(--text-muted)]">{h.response_time_ms}ms</span> + {#if h.error_message} + <button + onclick={() => (expandedError = expandedError === h.id ? null : h.id)} + class="text-[10px] text-[var(--red)] underline underline-offset-2 transition-colors hover:opacity-70" + > + {expandedError === h.id ? '▲ erro' : '▼ erro'} + </button> + {/if} + </div> + </div> + {#if expandedError === h.id && h.error_message} + <div + class="mt-1.5 overflow-auto rounded-md border border-[var(--red)]/20 bg-[var(--bg-secondary)]/50 p-2" + > + <pre class="break-all font-mono text-[11px] leading-relaxed text-[var(--text-muted)]" + >{h.error_message}</pre + > + </div> + {/if} + </div> + {/each} + </div> + </div> + </div> +{/if} |